package com.espirit.eap;

import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;
import org.apache.struts2.ServletActionContext;

import com.espirit.common.TxFile;
import com.espirit.eap.manager.I18nTextProvider;
import com.espirit.eap.manager.SystemData;
import com.espirit.eap.manager.SystemDataService;
import com.espirit.eap.manager.SystemLogService;
import com.espirit.eap.manager.workflow.WFInstanceService;
import com.espirit.eap.sa.LoginUserService;
import com.espirit.eap.sa.ValidationService;
import com.espirit.eap.util.TxFileService;
import com.googlecode.cswish.annotation.GenericType;
import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.model.PropertyInfo;
import com.googlecode.cswish.struts.DynamicAction;
import com.googlecode.cswish.struts.Routing;
import com.googlecode.cswish.struts.hibernate.DynamicClassLoaderFactory;
import com.googlecode.cswish.struts.interceptor.ExceptionMappingInterceptor;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.GenericService;
import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.FrameConstant;
import com.googlecode.cswish.util.Functions;
import com.googlecode.cswish.util.InvocationUtil;
import com.googlecode.cswish.util.InvocationUtil.ClassType;
import com.googlecode.cswish.util.InvocationUtil.ServiceInfo;
import com.googlecode.cswish.util.OgnlExUtil;
import com.googlecode.cswish.util.SimpleEncryptUtil;
import com.googlecode.cswish.util.bean.ModelUtil;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.LocaleProvider;
import com.opensymphony.xwork2.Result;
import com.opensymphony.xwork2.TextProvider;
import com.opensymphony.xwork2.ValidationAware;
import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;

/**
 * 1. init all the parameter: initialize
 * 2. set parameter value: prepareParameter (see invocationUtil.getDefaultParameters, invocationUtil.getInitValueByParameterType)
 * 3. invoke service: execute
 * 
 * ValidationAware, use by ExceptionMappingInterceptor, it calls addActionError to add method
 * 
 * @author Jerry
 *
 */
public class BaseActionSupportAction extends HashMap<String, Object> 
	implements DynamicAction, ValidationAware, TextProvider, LocaleProvider, Serializable {

	private static final long serialVersionUID = -8467866495111595155L;

	protected static Logger logger = LoggerFactory.getLogger(BaseActionSupportAction.class);
	
	// license check information
	public static int maxEnabledVisitCount;
	private static int visitCount;
	
	private Collection<String> actionErrors;
    private Collection<String> actionMessages;
    private Map<String, List<String>> fieldErrors;

    private I18nTextProvider textProvider;
    
    @Resource
    public void setI18nTextProvider(I18nTextProvider textProvider) {
    	this.textProvider = textProvider;
    	textProvider.setClazz(this.getClass());
    	textProvider.setLocaleProvider(this);
    }

    @Resource
	protected BeanIntrospector beanIntrospector;

    @Resource
	protected OgnlExUtil ognlExUtil;
    
    @Resource
    private Functions functions;
    
    @Resource
    private InvocationUtil invocationUtil;
    
    @Resource
    private WFInstanceService workflowService;
    
    @Resource
    private ValidationService validationService;
    
    @Resource
    private TxFileService txFileService;
    
    @Resource
    private SystemDataService systemDataService;
    
    @Resource
	private DynamicClassLoaderFactory dynamicClassLoaderFactory;
    
    @Resource
    private SystemLogService systemLogService;
    
    @Resource
    GenericService genericService;
    
    @Resource
    LoginUserService loginUserService;

    private final static Object NULL = new Object();
    public static final String ARRAY_OBJECTS_LENGTH = "cswish.NullHandler.arrayLength";

    // it can be null, such as the invocation path is xxx._search.html
	private ServiceInfo serviceInfo;
	
	private String actionName;
	
	public void initialize(String actionName, String pathWithSite) {
		if (pathWithSite.length() > 0 && pathWithSite.charAt(0) == '/') {
			pathWithSite = pathWithSite.substring(1);
		}
		String actionPath = Routing.getRequestPathNoSite(pathWithSite);
		super.put(FrameConstant.STACK_ACTIONPATH, actionPath);
		
		serviceInfo = invocationUtil.getServiceInfo(pathWithSite);
	}
	
	
	/**
	 * Invoke by ScopeParametersInterceptor
	 * 
	 * @param invocation
	 */
	public void prepareParameter(ActionInvocation invocation) {
		if (this.actionName == null) {
			Map<String, Object> context = invocation.getStack().getContext();
			actionName = (String) context.get(FrameConstant.STACK_ACTIONNAME);
			String path = invocation.getProxy().getActionName();
			
			initialize(actionName, path);
		}
		// prepare all the valid property, the ParametersInterceptor will call containsKey to check the parameter name
		super.putAll(invocationUtil.getDefaultParameters(serviceInfo, null, this));
	}
	
	/**
     * A default implementation that does nothing an returns "success".
     * <p/>
     * Subclasses should override this method to provide their business logic.
     * <p/>
     * See also {@link com.opensymphony.xwork2.Action#execute()}.
     *
     * @return returns {@link #SUCCESS}
     * @throws Exception  can be thrown by subclasses.
     */
    public String execute() throws Exception {
    	checkEnableVisitCount();
    	
    	try {
	    	dynamicClassLoaderFactory.bindSiteClassLoader();
	    	
	    	// Refer to DefaultWorkflowInterceptor.addPreResultListener
	    	
	    	// validate the user input
	    	validationService.validate(actionName, this);
	    	
	    	// TODO: use better transaction mechanism, see JBoss.TX File 
	    	List<String> uploadFiles = (List<String>) get(FrameConstant.STACK_UPLOADFILES);
	    	List<TxFile> txFiles = null;
	    	if (uploadFiles != null && uploadFiles.size() > 0) {
	    		txFiles = new ArrayList<TxFile>(uploadFiles.size());
	    		for (String filename : uploadFiles) {
	    			File file = (File)functions.val(this, filename);
	    			String fullFilePropertyName = filename + "FileName";
	    			String filePropertyName;
	    			Object model;
	    			int index = fullFilePropertyName.lastIndexOf('.');
	    			if (index > 0) {
	    				String beanName = fullFilePropertyName.substring(0, index);
	    				model = functions.val(this, beanName);
	    				filePropertyName = fullFilePropertyName.substring(index + 1);
	    			} else {
	    				model = this;
	    				filePropertyName = fullFilePropertyName;
	    			}
	    			if (model != null) {
		    			TxFile txFile = txFileService.convertToTxFile(model, fullFilePropertyName, filePropertyName, file);
		    			if (txFile != null) {
		    				txFiles.add(txFile);
		    			}
	    			}
	    		}
	    		
	    		put(FrameConstant.STACK_UPLOADFILES, txFiles);
	    	}
	    	HttpServletRequest request = ServletActionContext.getRequest();
	    	String commitFileName = request.getParameter(FrameConstant.PARAM_COMMIT_FILENAME);
	    	if (commitFileName != null && commitFileName.length() > 0) {
	    		// 发现二次提交的文件，将其还原到当前txFile列表
	    		txFiles = txFileService.resumeTxFiles(this, txFiles, commitFileName);
	    	}
	    	
	    	// add log
//	    	String methodName = StringUtils.uncapitalize(actionName.substring(actionName.lastIndexOf('.') + 1, actionName.length() - 6));
	    	
	    	if ("true".equals(systemDataService.getValue(SystemData.ENABLE_SYSTEMLOG, true, "false"))) {
//	    		String modelName = beanIntrospector.getSimpleName(actionName);
	    		// Action
//	    		String methodName = StringUtils.uncapitalize(actionName.substring(actionName.lastIndexOf('.') + 1, actionName.length() - 6));
//	    		String modelId = request.getParameter("model.id");
//	    		String parameter = modelId == null ? null : "{model.id=" + modelId + "}";
	    		
	    		String ip = request.getRemoteAddr();
//	    		String fromMenu = request.getParameter("fromMenu");
//	    		if (!(methodName.equals("_update") || methodName.equals("update"))){
	    		systemLogService.add(actionName, request.getParameterMap(), loginUserService.getLoginUser(), ip);
//	    			systemLogService.add(modelName, methodName, parameter, loginUserService.getLoginUser(), ip, fromMenu);
//	    		}
	    	}
	    	
	    	Object result = invocationUtil.invoke(serviceInfo, this, false);
	    	
	    	if (txFiles != null) {
	    		txFileService.commitTxFile(txFiles);
	    	}
	    	
			super.put("execute", result);
//			if (this.containsKey("appResult")){
////				Map<String, Object> datas = new HashMap<String, Object>();
////				datas.put("retCode", "200");
////				datas.put("message", "ok");
////				
////				Map<String, Object> data = new HashMap<String, Object>();
////				datas.put("data", data);
////				data.put("result", result);
////				data.put("sessionId", "abc");
////				data.put("permission", "ddd");
//				
//				super.put("app", super.get("appResult"));
//			}
			
	    	
			//String taskId = (String) super.get("taskId");
			workflowService.startProcessOrTask(actionName, this);
			
			if (result instanceof Result || result == null && serviceInfo != null && serviceInfo.isResult()) {
				return null;
			} else {
		    	/**
		    	 * Template is used to call another method template or child template of the method, it's generated by PageConvertHelper function
		    	 * about template, there's also a generic.$element.ftl page(_resultCode=$element), it can call the part of the template by the name,
		    	 *  the function is similar with template
		    	 */
		    	String templateName = (String) request.getParameter(FrameConstant.PARAM_TEMPLATE);
		    	if (templateName == null || templateName.isEmpty()) {
		    		return SUCCESS;
		    	} else {
		    		String resultCode = request.getParameter(FrameConstant.PARAM_RESULTCODE);
		    		if ("$element".equals(resultCode)) {
		    			return SUCCESS;
		    		} else {
		    			int index = templateName.indexOf(',');
		    			return SUCCESS + '.' + (index == -1 ? templateName : templateName.substring(0, index));
		    		}
		    	}
			}
    	} finally {
    		dynamicClassLoaderFactory.unbindSiteClassLoader();
    	}
    }

	private void checkEnableVisitCount() {
		if (maxEnabledVisitCount > 0 && visitCount < 100000) {	// 100,000 is enough for the license requirement
    		if (visitCount == 0) {
    			String value = systemDataService.getValue(SystemData.VISIT_COUNT);
    			if (value != null) {
    				String realValue = SimpleEncryptUtil.decrypt(value);
    				visitCount = Integer.parseInt(realValue);
    			}
    		}
    		
    		visitCount++;			// don't think about the synchronized problem, it's not exact statistics
    		
    		if (visitCount % 50 == 0) {
    			String encryptValue = SimpleEncryptUtil.encrypt(String.valueOf(visitCount));
    			systemDataService.setValue(SystemData.VISIT_COUNT, encryptValue);
    		}
    		
    		if (visitCount > maxEnabledVisitCount) {
    			ExceptionMappingInterceptor.interceptorEx = "error.licenseExpired";
    		}
    	}
	}
	
    @SuppressWarnings("rawtypes")
	@Override
    public Object get(Object key) {
    	Object value = super.get(key);
    	if (value == null && key != null) {
    		if (key.equals("actionErrors")) {
    			value = getActionErrors();
    		} else if (key.equals("actionMessages")) {
    			value = getActionMessages();
    		} else if (key.equals("fieldErrors")) {
    			value = getFieldErrors();
    		}
    		if (value == null) {
    			value = NULL;
    		}
    		super.put(key.toString(), value);
    	}
    	return value == NULL ? null : value;
    }

    
    @Override
    @SuppressWarnings({ "unchecked", "rawtypes" })
	public Object put(String key, Object value) {
    	if (serviceInfo != null && serviceInfo.parameteTypes != null && value != null) {
    		ClassType parameterType = serviceInfo.parameteTypes.get(key);
    		if (parameterType != null) {
    			// check if the input value matches the dest parameter type
		    	if (!parameterType.type.isAssignableFrom(value.getClass())) {
		    		XWorkConverter converter = ognlExUtil.getXWorkConverter();
					value = converter.convertValue(value, parameterType.type);
				} else {
					String genericType = parameterType.genericType;
					if (GenericType.PARAM_MODEL.equals(genericType) || GenericType.MODEL.equals(genericType)) {
						String type = ClassUtil.getRuntimeType(genericType);
						Class clazz = ClassUtil.forName(type);
						
						// ignore the unmatched value and create a new matched instance
						if (clazz != null && parameterType.type.isAssignableFrom(clazz) 
								&& !clazz.isAssignableFrom(value.getClass())) {
							value = ModelUtil.newInstance(clazz);
							// TODO: copy the non-null property value?
						}
					}
				}
    		}
    	}
    	
    	return super.put(key, value);
    }
    
    public ServiceInfo getServiceInfo() {
    	return serviceInfo;
    }
	
	 public void setActionErrors(Collection<String> errorMessages) {
        this.actionMessages = errorMessages;
    }

    public Collection<String> getActionErrors() {
    	if (this.actionErrors == null) {
    		this.actionErrors = new ArrayList<String>();
    	}
        return this.actionErrors;
    }

    public void setActionMessages(Collection<String> messages) {
        this.actionMessages = messages;
    }

    public Collection<String> getActionMessages() {
    	if (this.actionMessages == null) {
    		this.actionMessages = new ArrayList<String>();
    	}
        return this.actionMessages;
    }


    public void setFieldErrors(Map<String, List<String>> errorMap) {
        this.fieldErrors = errorMap;
    }

    public Map<String, List<String>> getFieldErrors() {
    	if (this.fieldErrors == null) {
    		this.fieldErrors = new HashMap<String, List<String>>();
    	}
        return this.fieldErrors;
    }

    public Locale getLocale() {
        ActionContext ctx = ActionContext.getContext();
        if (ctx != null) {
            return ctx.getLocale();
        } else {
            logger.debug("Action context not initialized");
            return null;
        }
    }

	public String getTextByLocale(String aTextName, String locale) {
		// Priority : locale --> current locale --> short key
		String text = textProvider.getTextByLocale(aTextName, locale, true);
		return text;
	}

    public String getText(String aTextName) {
        return getTextByLocale(aTextName, null);
    }
    
    public String getText(String aTextName, String defaultValue) {
    	return textProvider.getText(aTextName, defaultValue);
    }

    public String getText(String aTextName, String defaultValue, String obj) {
        return textProvider.getText(aTextName, defaultValue, obj);
    }

    public String getText(String aTextName, List args) {
        return textProvider.getText(aTextName, args);
    }

    public String getText(String key, String[] args) {
        return textProvider.getText(key, args);
    }

    public String getText(String aTextName, String defaultValue, List args) {
        return textProvider.getText(aTextName, defaultValue, args);
    }

    public String getText(String key, String defaultValue, String[] args) {
        return textProvider.getText(key, defaultValue, args);
    }

    public String getText(String key, String defaultValue, List args, ValueStack stack) {
        return textProvider.getText(key, defaultValue, args, stack);
    }

    public String getText(String key, String defaultValue, String[] args, ValueStack stack) {
        return textProvider.getText(key, defaultValue, args, stack);
    }

    public ResourceBundle getTexts() {
        return textProvider.getTexts();
    }

    public ResourceBundle getTexts(String aBundleName) {
        return textProvider.getTexts(aBundleName);
    }

    public void addActionError(String anErrorMessage) {
    	getActionErrors().add(anErrorMessage);
    }

    public void addActionMessage(String aMessage) {
    	getActionMessages().add(aMessage);
    }

    public void addFieldError(String fieldName, String errorMessage) {
    	List<String> thisFieldErrors = getFieldErrors().get(fieldName);
        if (thisFieldErrors == null) {
            thisFieldErrors = new ArrayList<String>();
            this.fieldErrors.put(fieldName, thisFieldErrors);
        }
        thisFieldErrors.add(errorMessage);
    }

    public String input() throws Exception {
        return INPUT;
    }
    
    public String doDefault() throws Exception {
        return SUCCESS;
    }

    public boolean hasActionErrors() {
        return this.actionErrors != null && !this.actionErrors.isEmpty();
    }

    public boolean hasActionMessages() {
        return this.actionMessages != null && !this.actionMessages.isEmpty();
    }

    public boolean hasErrors() {
    	return (hasActionErrors() || hasFieldErrors());
    }

    public boolean hasFieldErrors() {
    	return (fieldErrors != null) && !fieldErrors.isEmpty();
    }

    /**
     * Clears field errors. Useful for Continuations and other situations
     * where you might want to clear parts of the state on the same action.
     */
    public void clearFieldErrors() {
    	if (this.fieldErrors != null) {
    		this.fieldErrors.clear();
    	}
    }

    /**
     * Clears action errors. Useful for Continuations and other situations
     * where you might want to clear parts of the state on the same action.
     */
    public void clearActionErrors() {
        if (this.actionErrors != null) {
        	this.actionErrors.clear();
        }
    }

    /**
     * Clears messages. Useful for Continuations and other situations
     * where you might want to clear parts of the state on the same action.
     */
    public void clearMessages() {
        if (this.actionMessages != null) {
        	this.actionMessages.clear();
        }
    }

    /**
     * Clears all errors. Useful for Continuations and other situations
     * where you might want to clear parts of the state on the same action.
     */
    public void clearErrors() {
        clearActionErrors();
        clearFieldErrors();
    }

    /**
     * Clears all errors and messages. Useful for Continuations and other situations
     * where you might want to clear parts of the state on the same action.
     */
    public void clearErrorsAndMessages() {
    	clearActionErrors();
        clearFieldErrors();
        clearMessages();
    }

    /**
     * A default implementation that validates nothing.
     * Subclasses should override this method to provide validations.
     */
    public void validate() {
    }

    /**
     * <!-- START SNIPPET: pause-method -->
     * Stops the action invocation immediately (by throwing a PauseException) and causes the action invocation to return
     * the specified result, such as {@link #SUCCESS}, {@link #INPUT}, etc.
     * <p/>
     *
     * The next time this action is invoked (and using the same continuation ID), the method will resume immediately
     * after where this method was called, with the entire call stack in the execute method restored.
     * <p/>
     *
     * Note: this method can <b>only</b> be called within the {@link #execute()} method.
     * <!-- END SNIPPET: pause-method -->
     *
     * @param result the result to return - the same type of return value in the {@link #execute()} method.
     */
    public void pause(String result) {
    }

	@Override
	public Class getPropertyType(String name) {
		if (serviceInfo == null) {
			return null;
		} else {
			return serviceInfo.parameteTypes.get(name).type;
		}
	}

	@Override
	public Class getGenericPropertyType(String name) {
		if (serviceInfo == null) {
			return null;
		} else {
			String genericType = serviceInfo.parameteTypes.get(name).genericType;
			return ClassUtil.getClass(ClassUtil.getRuntimeType(genericType));
		}
	}

	@Override
	public String getActionName() {
		return actionName;
	}

	@Override
	public Collection<String> getProperties() {
		return serviceInfo.parameteTypes.keySet();
	}

	@Override
	public boolean hasKey(String key) {
		return textProvider.hasKey(key);
	}
}